package com.briup.server.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.briup.common.constant.SystemConstant;
import com.briup.common.response.ResultCode;
import com.briup.common.utils.AssertUtil;
import com.briup.common.utils.BeanUtil;
import com.briup.server.config.EmailSupporter;
import com.briup.server.domain.bean.*;
import com.briup.server.domain.dto.RentalAppointmentDTO;
import com.briup.server.domain.dto.RentalApprovalDTO;
import com.briup.server.domain.dto.RentalQueryDTO;
import com.briup.server.domain.po.RentalInfosPO;
import com.briup.server.domain.vo.RentalInfoVO;
import com.briup.server.domain.vo.RoleVO;
import com.briup.server.domain.vo.UserDetailVO;
import com.briup.server.mapper.*;
import com.briup.server.security.SecurityContext;
import com.briup.server.service.RentalInfosService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class RentalInfosServiceImpl extends ServiceImpl<RentalInfosMapper, RentalInfos> implements RentalInfosService {

    private final LabsMapper labsMapper;
    private final LabsAdminRMapper labsAdminRMapper;
    private final LabsRentalTimeMapper labsRentalTimeMapper;
    private final UserMapper userMapper;
    private final UserRoleMapper userRoleMapper;
    private final EmailSupporter emailSupporter;
    private final DateTimeFormatter pattern = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");

    @Override
    public void appointment(RentalAppointmentDTO dto) {
        // 确保预约确实可行后在进行添加
        checkEnable(dto);
        RentalInfos rentalInfos = BeanUtil.convert(dto, RentalInfos.class);
        // 设置默认审批状态为待审核
        rentalInfos.setApprovalStatus(0);
        // 设置申请人为当前登录用户
        rentalInfos.setRentalUserId(SecurityContext.userId());
        // 设置创建时间为当前时间
        rentalInfos.setCreateTime(LocalDateTime.now());
        // 插入预约信息
        save(rentalInfos);
        // 发送邮件通知预约人以及审批人
        sendMessage(rentalInfos);
    }

    private void sendMessage(RentalInfos rentalInfos) {
        // 预约人，即当前登录的用户
        UserDetailVO user = SecurityContext.currentUser();
        // 根据预约的实验室Id查询出对应的实验室管理员消息
        List<Long> userIds = labsAdminRMapper.selectList(
                        Wrappers.<LabsAdminR>lambdaQuery()
                                .eq(LabsAdminR::getLabsId, rentalInfos.getLabsId()))
                .stream().map(LabsAdminR::getAdminId)
                .collect(Collectors.toList());
        // 为了防止某一个实验室还未分配实验室管理员，所以发送邮件时，顺便通知一下系统管理员
        List<Long> adminIds = userRoleMapper.selectList(
                        Wrappers.<UserRole>lambdaQuery()
                                .eq(UserRole::getRoleId, SystemConstant.RoleConstant.ADMIN.getRoleId()))
                .stream().map(UserRole::getUserId).toList();
        // 合并所有用户id，并去重
        userIds.addAll(adminIds);
        userIds = userIds.stream().distinct().toList();
        // 根据用户id查询出对应的用户信息，即实验室管理员的信息
        List<User> users = userMapper.selectByIds(userIds);
        // 编写邮件内容
        String content = String.format("实验人员：%s 预约了实验室：%s，预约时间段：%s",
                user.getRealname(),
                labsMapper.selectById(rentalInfos.getLabsId()).getLabsName(),
                rentalInfos.getRentalDate() + " " +
                labsRentalTimeMapper.selectById(rentalInfos.getRentalTimeId()).getEnableTime());
        // 为预约人添加自己的邮箱，确保预约人也会收到邮件通知
        Set<String> emails = users.stream().map(User::getEmail).collect(Collectors.toSet());
        emails.add(user.getEmail());
        // 发送邮件通知预约人以及审批人
        emailSupporter.sendApprovalEmail(content, emails.toArray(new String[0]));
    }

    @Override
    public Page<RentalInfoVO> queryRentalInfos(RentalQueryDTO dto) {
        // 获取当前用户信息的角色信息，为了区分其查询数据的范围
        List<RoleVO> roles = SecurityContext.currentUser().getRoles();
        // 如果是管理员，还可以查询所有预约信息
        boolean isAdmin = roles.stream().anyMatch(
                role -> role.getName().equalsIgnoreCase(SystemConstant.ADMIN_ROLE_NAME));
        // 如果是实验室管理员，只能查询自己管理的实验室的预约信息
        boolean isLabsAdmin = roles.stream().anyMatch(
                role -> role.getName().equalsIgnoreCase(SystemConstant.LAB_ADMIN_ROLE_NAME));
        // 如果是实验室管理员，查询该用户管理的实验室
        List<String> labsIds = null;
        if (isLabsAdmin) {
            // 查询该用户管理的实验室
            labsIds = labsAdminRMapper.selectList(
                            Wrappers.<LabsAdminR>lambdaQuery()
                                    .eq(LabsAdminR::getAdminId, SecurityContext.userId()))
                    .stream().map(LabsAdminR::getLabsId).toList();
        }
        LambdaQueryWrapper<RentalInfos> wrapper = Wrappers.<RentalInfos>lambdaQuery()
                // 如果是管理员，查询所有预约信息，无需筛选
                .eq(dto.getApprovalStatus() != null,
                        RentalInfos::getApprovalStatus, dto.getApprovalStatus())
                // 如果是实验人员，只能查询自己的预约信息
                .eq(!isAdmin,
                        RentalInfos::getRentalUserId, SecurityContext.userId())
                // 因为实验管理员也可以是实验人员，可用预约其他实验室；所以这里应该是或的关系
                .or()
                // 如果是实验室管理员，只能查询自己管理的实验室的预约信息
                .in(!isAdmin && isLabsAdmin && CollectionUtils.isNotEmpty(labsIds),
                        RentalInfos::getLabsId, labsIds);

        // 分页信息
        Page<RentalInfos> page = new Page<>(dto.getPage(), dto.getSize());
        // 执行查询
        page(page, wrapper);
        Page<RentalInfoVO> voPage = convertToVO(page, dto.getQuery(), isAdmin, labsIds);
        // 设置分页总条数
        voPage.setTotal(count(wrapper));
        return voPage;
    }

    @Override
    public void approval(RentalApprovalDTO dto) {
        // 确保预约存在的情况下进行审核
        RentalInfos rentalInfos = getById(dto.getId());
        AssertUtil.notNull(rentalInfos, ResultCode.LABS_RENTAL_IS_NOT_EXISTS);
        // 当前用户即为审批人
        String username = SecurityContext.currentUser().getUsername();
        rentalInfos.setApprovalUser(SecurityContext.userId());
        // 设置实际的使用时间
        rentalInfos.setUseDate(rentalInfos.getRentalDate() + " " +
                               labsRentalTimeMapper.selectById(rentalInfos.getRentalTimeId()).getEnableTime());
        // 根据审批状态调整预约信息
        if (dto.getApproved()) {
            // 审核通过
            rentalInfos.setApprovalStatus(SystemConstant.ApprovalStatusConstant.APPROVED.getStatus());
            // 设置备注
            rentalInfos.setRemark(String.format(SystemConstant.APPROVAL_MESSAGE_SUCCESS_TEMPLATE,
                    rentalInfos.getUseDate(), labsMapper.selectById(rentalInfos.getLabsId()).getLabsName(),
                    username, pattern.format(LocalDateTime.now())));
        } else {
            // 审核不通过
            rentalInfos.setApprovalStatus(SystemConstant.ApprovalStatusConstant.REJECTED.getStatus());
            // 设置备注
            rentalInfos.setRemark(String.format(SystemConstant.APPROVAL_MESSAGE_FAILURE_TEMPLATE,
                    rentalInfos.getUseDate(), labsMapper.selectById(rentalInfos.getLabsId()).getLabsName(),
                    username, dto.getReason(), pattern.format(LocalDateTime.now())));
        }
        // 更新预约信息
        updateById(rentalInfos);
        // 发送审核结果通知预约人
        sendApprovalEmail(rentalInfos, dto.getApproved());
    }

    @Override
    public void autoApproval() {
        // 查询所有待审核的预约信息，必须是今天的预约，
        // 并且是距离预约时间小于等于30分钟内的预约
        List<RentalInfosPO> rentalInfos = baseMapper.queryList();
        // 按照实验室分组，并且各个分组内部按照预约创建时间排序，先预约的排在前面
        Map<String, List<RentalInfosPO>> groupedRentalInfos = rentalInfos.stream()
                .collect(Collectors.groupingBy(RentalInfosPO::getLabsName));
        // 遍历每个实验室的预约信息
        groupedRentalInfos.forEach((labsName, infos) -> {
            // 自动审批预约信息，每个分组内第一个预约的自动通过，其他预约的自动拒绝
            RentalInfosPO infosPO = infos.remove(0);
            // 修改表中数据，将预约状态设置为已通过
            RentalInfos entity = poConvertToEntity(infosPO, true,
                    (String.format(SystemConstant.APPROVAL_MESSAGE_SUCCESS_TEMPLATE,
                            LocalDate.now(), labsName,
                            "系统管理员", pattern.format(LocalDateTime.now()))));
            // 更新预约信息
            updateById(entity);
            // 发送审核结果通知预约人
            String content = String.format("实验人员：%s 预约了实验室：%s，预约时间段：%s，%s审核，" +
                                           "审核人为：%s，审核时间为：%s。",
                    infosPO.getRentalUserName(), labsName, infosPO.getUseDate(), "系统自动通过",
                    "系统管理员",
                    pattern.format(LocalDateTime.now()));
            emailSupporter.sendEmail(infosPO.getRentalUserEmail(), "实验室预约审批结果", content);
            // 遍历剩余预约信息，自动拒绝
            infos.forEach(info -> {
                // 修改表中数据，将预约状态设置为已拒绝
                RentalInfos entity2 = poConvertToEntity(info, false,
                        (String.format(SystemConstant.APPROVAL_MESSAGE_FAILURE_TEMPLATE,
                                LocalDate.now(), labsName,
                                "系统管理员", "与其余预约冲突，请重新预约", pattern.format(LocalDateTime.now()))));
                // 更新预约信息
                updateById(entity2);
                // 发送审核结果通知预约人
                String content2 = String.format("实验人员：%s 预约了实验室：%s，预约时间段：%s，%s审核，" +
                                                "审核人为：%s，审核时间为：%s。",
                        info.getRentalUserName(), labsName, info.getUseDate(), "系统自动拒绝",
                        "系统管理员",
                        pattern.format(LocalDateTime.now()));
                emailSupporter.sendEmail(info.getRentalUserEmail(), "实验室预约审批结果", content2);
            });
        });
    }

    private RentalInfos poConvertToEntity(RentalInfosPO po, boolean approved, String remark) {
        RentalInfos entity = new RentalInfos();
        entity.setId(po.getId());
        entity.setApprovalStatus(approved ?
                SystemConstant.ApprovalStatusConstant.APPROVED.getStatus() :
                SystemConstant.ApprovalStatusConstant.REJECTED.getStatus());
        entity.setRemark(remark);
        entity.setUseDate(po.getUseDate());
        // 设置审批人，默认系统管理员
        entity.setApprovalUser(1L);
        return entity;
    }

    private void sendApprovalEmail(RentalInfos rentalInfos, boolean approved) {
        User user = userMapper.selectById(rentalInfos.getRentalUserId());
        // 编写邮件内容
        String content = String.format("实验人员：%s 预约了实验室：%s，预约时间段：%s，%s审核，" +
                                       "审核人为：%s，审核时间为：%s。",
                user.getRealname(),
                labsMapper.selectById(rentalInfos.getLabsId()).getLabsName(),
                rentalInfos.getRentalDate() + " " +
                labsRentalTimeMapper.selectById(rentalInfos.getRentalTimeId()).getEnableTime(),
                approved ? "已通过" : "未通过",
                SecurityContext.currentUser().getRealname(),
                pattern.format(LocalDateTime.now()));
        // 只需要给预约人员发送邮件通知即可
        emailSupporter.sendEmail(user.getEmail(), "实验室预约审批结果", content);
    }

    /**
     * 将RentalInfos实体转换为RentalInfoVO视图对象
     *
     * @param page    分页结果
     * @param query   查询字符串
     * @param labsIds 实验室ID列表，主要用于判断是否为实验室管理员
     *                （如果存在那么表示当前用户为实验室管理员，其下的实验室预约列表具有审批权限，
     *                不是自己管理的实验室，没有审批权限）
     * @param isAdmin 是否具有审批权限，主要使用是否为超级管理员来判断是否具有审批权限
     * @return 分页的RentalInfoVO视图对象
     */
    private Page<RentalInfoVO> convertToVO(Page<RentalInfos> page, String query, boolean isAdmin, List<String> labsIds) {
        Page<RentalInfoVO> voPage = new Page<>(page.getCurrent(), page.getSize());
        List<RentalInfoVO> voList = page.getRecords().stream().map(rentalInfo -> {
                    RentalInfoVO vo = BeanUtil.convert(rentalInfo, RentalInfoVO.class);
                    // 设置实验室名称
                    Labs labs = labsMapper.selectById(rentalInfo.getLabsId());
                    vo.setLabsName(labs.getLabsName());
                    // 设置预约人名字
                    vo.setRentalUserName(userMapper.selectById(rentalInfo.getRentalUserId()).getUsername());
                    // 设置预约时间范围
                    vo.setRentalTimeSlot(labsRentalTimeMapper.selectById(rentalInfo.getRentalTimeId()).getEnableTime());
                    // 如果是管理员，或者是实验室管理员，设置为具有审批权限
                    if (isAdmin || CollectionUtils.isNotEmpty(labsIds) && labsIds.contains(rentalInfo.getLabsId())) {
                        vo.setHasApprovalPermission(true);
                    }
                    return vo;
                })
                // 筛选出符合查询条件的预约信息
                .filter(vo ->
                        !StringUtils.hasText(query) ||
                        vo.getLabsName().toLowerCase().contains(query.toLowerCase()) ||
                        vo.getRentalUserName().toLowerCase().contains(query.toLowerCase()))
                .toList();
        // 设置分页结果的记录列表
        voPage.setRecords(voList
                .stream().skip((page.getCurrent() - 1) * page.getSize())
                .limit(page.getSize())
                .toList());
        return voPage;
    }

    private void checkEnable(RentalAppointmentDTO dto) {
        // 确保实验室存在
        Labs labs = labsMapper.selectById(dto.getLabsId());
        AssertUtil.notNull(labs, ResultCode.LABS_IS_NOT_EXISTS);
        // 确保预约时间段存在
        LabsRentalTime rentalTime = labsRentalTimeMapper.selectById(dto.getRentalTimeId());
        AssertUtil.notNull(rentalTime, ResultCode.LABS_RENTAL_TIME_NOT_FOUND);
        // 同时确保，如果选择的日期是当天，预约时间不能在当前时间之前
        if (dto.getRentalDate().equals(LocalDate.now())) {
            // 确保预约时间在可用时间段内
            LocalTime now = LocalTime.now();
            String[] times = rentalTime.getEnableTime().split("-");
            LocalTime startTime = LocalTime.parse(times[0]);
            LocalTime endTime = LocalTime.parse(times[1]);
            // 确保预约时间在可用时间段内
            AssertUtil.isTrue(now.isBefore(startTime) && now.isBefore(endTime), ResultCode.LABS_RENTAL_TIME_NOT_AVAILABLE);
        }
    }
}